UNPKG

@defikitdotnet/education-module-ai

Version:
833 lines (832 loc) 73.7 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); var jsx_runtime_1 = require("react/jsx-runtime"); var react_1 = require("react"); var router_1 = require("next/router"); var link_1 = __importDefault(require("next/link")); var dynamic_1 = __importDefault(require("next/dynamic")); var react_markdown_1 = __importDefault(require("react-markdown")); var button_1 = require("../../../components/ui/button"); var card_1 = require("../../../components/ui/card"); var sonner_1 = require("sonner"); // Import components var H5PContent = (0, dynamic_1.default)(function () { return Promise.resolve().then(function () { return __importStar(require("../../../components/H5PContent")); }); }, { ssr: false, }); var StudentContentView = function () { console.log("--- StudentContentView Component Render Start ---"); var router = (0, router_1.useRouter)(); var courseId = router.query.id; // Rename id to courseId for clarity var _a = (0, react_1.useState)(null), courseData = _a[0], setCourseData = _a[1]; var _b = (0, react_1.useState)(null), currentContentItem = _b[0], setCurrentContentItem = _b[1]; var _c = (0, react_1.useState)(true), loading = _c[0], setLoading = _c[1]; var _d = (0, react_1.useState)(null), error = _d[0], setError = _d[1]; var studentId = (0, react_1.useState)("student-1")[0]; // TODO: Replace with actual authenticated student ID var _e = (0, react_1.useState)(new Map()), studentAnswers = _e[0], setStudentAnswers = _e[1]; // Map questionId -> answer var _f = (0, react_1.useState)(false), submitting = _f[0], setSubmitting = _f[1]; var _g = (0, react_1.useState)(null), submissionResult = _g[0], setSubmissionResult = _g[1]; // Use the DTO type var _h = (0, react_1.useState)(false), showAnswers = _h[0], setShowAnswers = _h[1]; // State for showing answers var _j = (0, react_1.useState)(null), h5pScore = _j[0], setH5pScore = _j[1]; var _k = (0, react_1.useState)(null), enrollment = _k[0], setEnrollment = _k[1]; var _l = (0, react_1.useState)(new Set()), completedLectures = _l[0], setCompletedLectures = _l[1]; var _m = (0, react_1.useState)(false), updatingProgress = _m[0], setUpdatingProgress = _m[1]; var _o = (0, react_1.useState)(true), isAuthenticated = _o[0], setIsAuthenticated = _o[1]; // Default to true initially var _p = (0, react_1.useState)(true), isAuthLoading = _p[0], setIsAuthLoading = _p[1]; // Add auth loading state var _q = (0, react_1.useState)(null), student = _q[0], setStudent = _q[1]; // Add student state var _r = (0, react_1.useState)(new Set()), completedQuizzes = _r[0], setCompletedQuizzes = _r[1]; // Track completed quizzes var apiBaseUrl = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:3009"; var teacherId = router.query.teacherId || (courseData === null || courseData === void 0 ? void 0 : courseData.teacherId) || ""; // Get teacherId for navigation // --- DEBUG LOG: Log all state on every render --- console.log("--- StudentContentView Render ---"); console.log("State:", { courseId: courseId, loading: loading, error: error, studentId: studentId, currentContentItemId: currentContentItem === null || currentContentItem === void 0 ? void 0 : currentContentItem.id, studentAnswersSize: studentAnswers.size, isSubmitting: submitting, hasSubmissionResult: !!submissionResult, courseDataLoaded: !!courseData, }); console.log("Current Answers Map:", studentAnswers); console.log("--- End Render Log ---"); // --- END DEBUG LOG --- // Fetch Course structure data (0, react_1.useEffect)(function () { var fetchCourseData = function () { return __awaiter(void 0, void 0, void 0, function () { var response, errorData, data, err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!courseId || typeof courseId !== "string") { setError("Invalid Course ID."); setLoading(false); return [2 /*return*/]; } _a.label = 1; case 1: _a.trys.push([1, 6, 7, 8]); setLoading(true); setError(null); return [4 /*yield*/, fetch("".concat(apiBaseUrl, "/api/courses/").concat(courseId))]; case 2: response = _a.sent(); if (!!response.ok) return [3 /*break*/, 4]; return [4 /*yield*/, response.json().catch(function () { return ({}); })]; case 3: errorData = _a.sent(); throw new Error(errorData.error || "Failed to fetch course data (Status: ".concat(response.status, ")")); case 4: return [4 /*yield*/, response.json()]; case 5: data = _a.sent(); setCourseData(data); // Set the initial content item (e.g., first lecture of first section) if (data.sections && data.sections.length > 0 && data.sections[0].lectures && data.sections[0].lectures.length > 0) { setCurrentContentItem(data.sections[0].lectures[0]); } else if (data.sections && data.sections.length > 0 && data.sections[0].quiz) { // If first section has no lectures but has a quiz setCurrentContentItem(data.sections[0].quiz); } return [3 /*break*/, 8]; case 6: err_1 = _a.sent(); console.error("Error fetching course data:", err_1); setError(err_1.message || "Failed to load course data. Please try again."); return [3 /*break*/, 8]; case 7: setLoading(false); return [7 /*endfinally*/]; case 8: return [2 /*return*/]; } }); }); }; fetchCourseData(); }, [courseId, apiBaseUrl]); // Depend on courseId // Fetch enrollment and progress data for the student and course (0, react_1.useEffect)(function () { var fetchEnrollmentData = function () { return __awaiter(void 0, void 0, void 0, function () { var progressResponse, _a, enrollment_1, completedLectureIds, completedQuizIds, lectureIds, quizIds, foundFirstUncompleted, mostRecentCompleted, _i, _b, section, _c, _d, lecture, errorData, err_2; return __generator(this, function (_e) { switch (_e.label) { case 0: // Wait for auth check to complete and confirm authentication if (!courseId || typeof courseId !== "string" || isAuthLoading || !isAuthenticated || !courseData) return [2 /*return*/]; _e.label = 1; case 1: _e.trys.push([1, 8, , 9]); console.log("[Fetch Enrollment] Starting to fetch enrollment data..."); return [4 /*yield*/, fetch("".concat(apiBaseUrl, "/api/enrollments/progress/").concat(courseId), { credentials: "include" })]; case 2: progressResponse = _e.sent(); if (!progressResponse.ok) return [3 /*break*/, 4]; return [4 /*yield*/, progressResponse.json()]; case 3: _a = _e.sent(), enrollment_1 = _a.enrollment, completedLectureIds = _a.completedLectures, completedQuizIds = _a.completedQuizzes; console.log("[Fetch Enrollment] API Response OK:", { enrollment: enrollment_1, completedLectureIds: completedLectureIds, completedQuizIds: completedQuizIds, isArray: Array.isArray(completedLectureIds), length: (completedLectureIds === null || completedLectureIds === void 0 ? void 0 : completedLectureIds.length) || 0 }); setEnrollment(enrollment_1); lectureIds = new Set(Array.isArray(completedLectureIds) ? completedLectureIds : []); console.log("[Fetch Enrollment] Setting completedLectures Set:", lectureIds); setCompletedLectures(lectureIds); quizIds = new Set(Array.isArray(completedQuizIds) ? completedQuizIds : []); console.log("[Fetch Enrollment] Setting completedQuizzes Set:", quizIds); setCompletedQuizzes(quizIds); // Find the first uncompleted lecture and set it as the current content item if (courseData && (lectureIds.size > 0 || quizIds.size > 0)) { foundFirstUncompleted = false; mostRecentCompleted = null; // Look through all sections for (_i = 0, _b = courseData.sections; _i < _b.length; _i++) { section = _b[_i]; // First try to find the first uncompleted lecture for (_c = 0, _d = section.lectures; _c < _d.length; _c++) { lecture = _d[_c]; // If we've found an uncompleted lecture, set it as current and exit if (!lectureIds.has(lecture.id)) { console.log("[Navigation] Setting current item to first uncompleted lecture:", lecture.title); setCurrentContentItem(lecture); foundFirstUncompleted = true; break; } else { // Track the most recent completed lecture mostRecentCompleted = lecture; } } if (foundFirstUncompleted) break; // If all lectures in this section are complete, check if there's a quiz if (section.quiz && !foundFirstUncompleted) { // If quiz is not completed, navigate to it if (!quizIds.has(section.quiz.id)) { console.log("[Navigation] Setting current item to uncompleted quiz:", section.quiz.title); setCurrentContentItem(section.quiz); foundFirstUncompleted = true; break; } } } // If all lectures are completed, set to the most recent one if (!foundFirstUncompleted && mostRecentCompleted) { console.log("[Navigation] All lectures completed, setting to most recent lecture:", mostRecentCompleted.title); setCurrentContentItem(mostRecentCompleted); } } return [3 /*break*/, 7]; case 4: if (!(progressResponse.status === 404)) return [3 /*break*/, 5]; // Student is likely not enrolled (or auth issue server-side) // Don't redirect immediately, show an error or a specific message console.warn("Enrollment not found (404). User might not be enrolled or auth failed for API."); setError("Enrollment not found. Please ensure you are enrolled in this course."); setEnrollment(null); // Ensure enrollment state is null return [3 /*break*/, 7]; case 5: return [4 /*yield*/, progressResponse.json().catch(function () { return ({}); })]; case 6: errorData = _e.sent(); throw new Error(errorData.error || "Failed to fetch enrollment data (Status: ".concat(progressResponse.status, ")")); case 7: return [3 /*break*/, 9]; case 8: err_2 = _e.sent(); console.error("Error fetching enrollment data:", err_2); setError(err_2.message || "Failed to load enrollment status."); setEnrollment(null); // Ensure enrollment state is null on error setCompletedLectures(new Set()); // Ensure it's empty on error console.log("[Fetch Enrollment] Error setting completedLectures state Set to empty."); // <<< ADDED LOG return [3 /*break*/, 9]; case 9: return [2 /*return*/]; } }); }); }; fetchEnrollmentData(); // Add isAuthLoading and isAuthenticated as dependencies }, [courseId, apiBaseUrl, isAuthLoading, isAuthenticated, courseData]); // --- NEW: Function to explicitly mark the current lecture as complete --- var markCurrentLectureComplete = (0, react_1.useCallback)(function () { return __awaiter(void 0, void 0, void 0, function () { var lecture, response, _a, progress_1, isCompleted_1, errorData, err_3; return __generator(this, function (_b) { switch (_b.label) { case 0: // Only proceed if the current item is a lecture and not already completed if (!currentContentItem || currentContentItem.hasOwnProperty('questions') || completedLectures.has(currentContentItem.id) || updatingProgress) { console.log('[Mark Complete Trigger] Skipping:', { isLecture: currentContentItem && !currentContentItem.hasOwnProperty('questions'), isCompleted: currentContentItem && completedLectures.has(currentContentItem.id), updatingProgress: updatingProgress }); return [2 /*return*/]; } lecture = currentContentItem; console.log("[Mark Complete Trigger] Attempting to mark lecture ".concat(lecture.id, " as complete.")); setUpdatingProgress(true); _b.label = 1; case 1: _b.trys.push([1, 7, 8, 9]); return [4 /*yield*/, fetch("".concat(apiBaseUrl, "/api/enrollments/progress/complete-lecture"), { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ lectureId: lecture.id, courseId: courseId, }), })]; case 2: response = _b.sent(); if (!response.ok) return [3 /*break*/, 4]; return [4 /*yield*/, response.json()]; case 3: _a = _b.sent(), progress_1 = _a.progress, isCompleted_1 = _a.isCompleted; console.log('[Mark Complete] API Response OK:', { progress: progress_1, isCompleted: isCompleted_1 }); // Add to completed lectures set locally setCompletedLectures(function (prev) { var newSet = new Set(prev); newSet.add(lecture.id); console.log('[Mark Complete] Updated completedLectures Set:', newSet); return newSet; }); // Update enrollment state with new progress percentage from backend setEnrollment(function (prev) { if (!prev) return prev; var updatedEnrollment = __assign(__assign({}, prev), { progress: progress_1, completed: isCompleted_1 // Use completion status from backend response }); console.log('[Mark Complete] Updated enrollment state:', updatedEnrollment); return updatedEnrollment; }); // If course is completed, show congratulations message if (isCompleted_1) { sonner_1.toast.success("Congratulations! You've completed this course!"); } return [3 /*break*/, 6]; case 4: return [4 /*yield*/, response.json().catch(function () { return ({}); })]; case 5: errorData = _b.sent(); console.error("[Mark Complete] API Error (".concat(response.status, "):"), errorData); sonner_1.toast.error(errorData.message || "Failed to mark lecture complete (Status: ".concat(response.status, ")")); _b.label = 6; case 6: return [3 /*break*/, 9]; case 7: err_3 = _b.sent(); console.error("[Mark Complete] Fetch Error:", err_3); sonner_1.toast.error("An error occurred while saving progress."); return [3 /*break*/, 9]; case 8: setUpdatingProgress(false); return [7 /*endfinally*/]; case 9: return [2 /*return*/]; } }); }); }, [currentContentItem, courseId, apiBaseUrl, completedLectures, updatingProgress, setCompletedLectures, setEnrollment, sonner_1.toast]); // Dependencies for the callback // Initialize or reset answers when a quiz is selected var initializeAnswers = (0, react_1.useCallback)(function (quiz) { console.log("Initializing/Resetting answers for quiz:", quiz.id); // DEBUG LOG var initialAnswers = new Map(); quiz.questions.forEach(function (q) { initialAnswers.set(q.id, { questionId: q.id, answer: "" }); }); setStudentAnswers(initialAnswers); setSubmissionResult(null); // Clear previous results setError(null); // Clear previous submission errors }, []); // Added useCallback with empty dependency array // Update state when a new content item is selected from sidebar var handleContentSelect = function (item) { console.log("handleContentSelect: Setting current item to:", item.id, item.title); // Keep this log setCurrentContentItem(item); }; // --- NEW useEffect to initialize answers when currentContentItem is a quiz --- (0, react_1.useEffect)(function () { if (currentContentItem && "questions" in currentContentItem) { // Check if it's a Quiz console.log("currentContentItem changed to a Quiz, initializing answers:", currentContentItem.id); // DEBUG LOG initializeAnswers(currentContentItem); } else if (currentContentItem) { // It's a lecture or other non-quiz item console.log("currentContentItem changed to a Lecture/Other, clearing answers."); // DEBUG LOG setStudentAnswers(new Map()); // Clear answers if it's not a quiz setSubmissionResult(null); // Clear results setError(null); // Clear errors } // This effect should run ONLY when currentContentItem changes }, [currentContentItem, initializeAnswers]); // --- Answer Handling (Modified for Quiz) --- var handleQuizAnswerChange = function (questionId, answer) { console.log("handleQuizAnswerChange called for question ".concat(questionId, " with answer:"), answer); // DEBUG LOG setStudentAnswers(function (prev) { var newState = new Map(prev).set(questionId, { questionId: questionId, answer: answer }); console.log("New studentAnswers state:", newState); // DEBUG LOG return newState; }); }; // Handle submitting QUIZ answers var handleSubmitQuiz = function () { return __awaiter(void 0, void 0, void 0, function () { var quizId, answersPayload, unansweredQuestions, response, responseData, quizId_1, quizCompletionResponse, _a, progress_2, isCompleted_2, err_4, err_5, errorMessage; return __generator(this, function (_b) { switch (_b.label) { case 0: // Ensure current item is a quiz and has questions if (!currentContentItem || !("questions" in currentContentItem) || currentContentItem.questions.length === 0) { sonner_1.toast.error("Cannot submit, no quiz selected or quiz has no questions."); return [2 /*return*/]; } quizId = currentContentItem.id; answersPayload = Array.from(studentAnswers.values()); unansweredQuestions = answersPayload.filter(function (ans) { return ans.answer === "" || ans.answer === undefined || ans.answer === null; }); if (unansweredQuestions.length > 0) { sonner_1.toast.warning("Please answer all ".concat(currentContentItem.questions.length, " questions before submitting. (").concat(unansweredQuestions.length, " remaining)")); return [2 /*return*/]; } _b.label = 1; case 1: _b.trys.push([1, 12, 13, 14]); setSubmitting(true); setError(null); setSubmissionResult(null); setShowAnswers(false); // Reset show answers on new submission return [4 /*yield*/, fetch("".concat(apiBaseUrl, "/api/student/quizzes/").concat(quizId, "/submit"), { method: "POST", headers: { "Content-Type": "application/json", // Add authentication headers if required by your backend (e.g., JWT token) // 'Authorization': `Bearer ${yourAuthToken}`, }, credentials: "include", // Ensure cookies are sent body: JSON.stringify({ // Use the actual student ID from auth state studentId: student === null || student === void 0 ? void 0 : student.id, // Use the student ID from state answers: answersPayload, }), })]; case 2: response = _b.sent(); return [4 /*yield*/, response.json()]; case 3: responseData = _b.sent(); if (!response.ok) { // Throw an error with the message from the backend if available throw new Error(responseData.error || responseData.message || "HTTP error! status: ".concat(response.status)); } if (!responseData.data) return [3 /*break*/, 10]; setSubmissionResult(responseData.data); sonner_1.toast.success(responseData.message || "Quiz submitted successfully!"); if (!(currentContentItem && "questions" in currentContentItem)) return [3 /*break*/, 9]; quizId_1 = currentContentItem.id; setCompletedQuizzes(function (prev) { var newSet = new Set(prev); newSet.add(quizId_1); console.log("[Quiz Complete] Added quiz to completedQuizzes:", quizId_1); return newSet; }); _b.label = 4; case 4: _b.trys.push([4, 8, , 9]); return [4 /*yield*/, fetch("".concat(apiBaseUrl, "/api/enrollments/quiz-complete"), { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ quizId: quizId_1, courseId: courseId, score: responseData.data.score }), })]; case 5: quizCompletionResponse = _b.sent(); if (!quizCompletionResponse.ok) return [3 /*break*/, 7]; return [4 /*yield*/, quizCompletionResponse.json()]; case 6: _a = _b.sent(), progress_2 = _a.progress, isCompleted_2 = _a.isCompleted; console.log("[Quiz Complete] Progress updated:", { progress: progress_2, isCompleted: isCompleted_2 }); // Update enrollment with new progress setEnrollment(function (prev) { if (!prev) return prev; return __assign(__assign({}, prev), { progress: progress_2, completed: isCompleted_2 // Use completion status from backend response }); }); _b.label = 7; case 7: return [3 /*break*/, 9]; case 8: err_4 = _b.sent(); console.error("Error recording quiz completion:", err_4); return [3 /*break*/, 9]; case 9: return [3 /*break*/, 11]; case 10: // Handle unexpected success response format console.warn("Quiz submission successful, but response data format unexpected:", responseData); sonner_1.toast.success("Quiz submitted, but couldn't display results."); _b.label = 11; case 11: return [3 /*break*/, 14]; case 12: err_5 = _b.sent(); console.error("Error submitting quiz answers:", err_5); errorMessage = err_5.message || "Failed to submit quiz answers. Please try again."; setError(errorMessage); sonner_1.toast.error(errorMessage); return [3 /*break*/, 14]; case 13: setSubmitting(false); return [7 /*endfinally*/]; case 14: return [2 /*return*/]; } }); }); }; // Handle H5P completion (Assuming H5P is linked to a lecture) var handleH5PCompleted = function (score, maxScore) { if (!currentContentItem || "questions" in currentContentItem) return; // Only for lectures var lectureId = currentContentItem.id; setH5pScore({ score: score, maxScore: maxScore }); sonner_1.toast.info("H5P score recorded: ".concat(score, "/").concat(maxScore)); // TODO: Need a backend endpoint to record H5P completion for a specific lecture attempt // Example: /api/student/lectures/:lectureId/h5p-complete console.log("Recording H5P completion:", { lectureId: lectureId, studentId: student === null || student === void 0 ? void 0 : student.id, score: score, maxScore: maxScore, }); fetch("".concat(apiBaseUrl, "/api/student/lectures/").concat(lectureId, "/h5p-complete"), { // Hypothetical endpoint method: "POST", headers: { "Content-Type": "application/json", credentials: "include" }, // Add credentials body: JSON.stringify({ studentId: student === null || student === void 0 ? void 0 : student.id, score: score, maxScore: maxScore }), // Use student ID from state }).catch(function (err) { console.error("Error recording H5P completion:", err); sonner_1.toast.error("Failed to save H5P progress."); }); }; // Format date for display var formatDate = function (dateString) { if (!dateString) return "N/A"; try { return new Date(dateString).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", }); } catch (e) { return "Invalid Date"; } }; // Helper to parse JSON safely var safeJsonParse = function (jsonString, defaultValue) { if (defaultValue === void 0) { defaultValue = null; } if (!jsonString || typeof jsonString !== "string") { console.warn("safeJsonParse: Input is not a valid string", jsonString); return defaultValue; } try { return JSON.parse(jsonString.trim()); } catch (e) { console.error("safeJsonParse: Failed to parse JSON string:", jsonString, e); return defaultValue; // Return default if parsing fails } }; // --- Action Handlers for Post-Quiz Options --- var handleRetakeQuiz = function () { if (!currentContentItem || !("questions" in currentContentItem)) return; console.log("Retaking quiz:", currentContentItem.id); setSubmissionResult(null); // Clear previous result setShowAnswers(false); // Hide answers view setError(null); // Clear previous errors initializeAnswers(currentContentItem); // Re-initialize/clear answers map sonner_1.toast.info("Quiz reset. Please answer the questions again."); }; var handleShowAnswersClick = function () { console.log("Showing answers..."); setShowAnswers(true); // Check if backend provided answer details if (!(submissionResult === null || submissionResult === void 0 ? void 0 : submissionResult.answerDetails) || submissionResult.answerDetails.length === 0) { // This might happen if backend failed to generate details sonner_1.toast.warning("Detailed answer explanations could not be loaded."); } else { sonner_1.toast.info("Showing detailed answers."); } }; var findNextContentItem = function () { var _a; if (!courseData || !currentContentItem) return null; var currentSectionIndex = -1; var currentItemIndex = -1; var isQuiz = "questions" in currentContentItem; // Find the current section and item index for (var i = 0; i < courseData.sections.length; i++) { var section = courseData.sections[i]; if (isQuiz && ((_a = section.quiz) === null || _a === void 0 ? void 0 : _a.id) === currentContentItem.id) { currentSectionIndex = i; currentItemIndex = section.lectures.length; // Quiz is considered after all lectures in a section break; } else if (!isQuiz) { var lectureIndex = section.lectures.findIndex(function (l) { return l.id === currentContentItem.id; }); if (lectureIndex !== -1) { currentSectionIndex = i; currentItemIndex = lectureIndex; break; } } } if (currentSectionIndex === -1) return null; // Should not happen var currentSection = courseData.sections[currentSectionIndex]; // Check if there's a next lecture in the current section if (currentItemIndex < currentSection.lectures.length - 1) { return currentSection.lectures[currentItemIndex + 1]; } // Check if there's a quiz after the last lecture in the current section if (!isQuiz && currentItemIndex === currentSection.lectures.length - 1 && currentSection.quiz) { return currentSection.quiz; } // Check if there's a next section if (currentSectionIndex < courseData.sections.length - 1) { var nextSection = courseData.sections[currentSectionIndex + 1]; // Return first lecture of next section, or its quiz if no lectures if (nextSection.lectures.length > 0) { return nextSection.lectures[0]; } else if (nextSection.quiz) { return nextSection.quiz; } } // No more content items return null; }; var handleNextSection = function () { return __awaiter(void 0, void 0, void 0, function () { var nextItem; return __generator(this, function (_a) { switch (_a.label) { case 0: // Make async // *** Mark current lecture complete BEFORE navigating *** return [4 /*yield*/, markCurrentLectureComplete()]; case 1: // *** Mark current lecture complete BEFORE navigating *** _a.sent(); nextItem = findNextContentItem(); if (nextItem) { console.log("Navigating to next item:", nextItem.id, nextItem.title); handleContentSelect(nextItem); // Reset quiz-specific states if navigating away from a quiz result setShowAnswers(false); // submissionResult is kept to show completion status maybe? Or clear it? // setSubmissionResult(null); // Optional: clear result when moving on } else { // This case should ideally be handled by the Finish Course button now console.log("No next section found. (handleNextSection)"); sonner_1.toast.success("Congratulations! You have completed the course content."); // Navigate back to dashboard if button wasn't clicked for some reason router.push("/student?teacherId=".concat(teacherId)); } return [2 /*return*/]; } }); }); }; var handleFinishCourse = function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { switch (_a.label) { case 0: // Make async // *** Mark current lecture complete BEFORE finishing *** return [4 /*yield*/, markCurrentLectureComplete()]; case 1: // *** Mark current lecture complete BEFORE finishing *** _a.sent(); console.log("Finishing course and returning to dashboard..."); sonner_1.toast.success("Congratulations! Course completed."); // Reset state related to the current quiz before navigating setSubmissionResult(null); setShowAnswers(false); setStudentAnswers(new Map()); // Navigate back to the student dashboard, ensuring teacherId is passed if needed router.push("/student?teacherId=".concat(teacherId)); return [2 /*return*/]; } }); }); }; // Check authentication status and get student data (0, react_1.useEffect)(function () { var checkAuthStatus = function () { return __awaiter(void 0, void 0, void 0, function () { var response, data, errorText, err_6; return __generator(this, function (_a) { switch (_a.label) { case 0: setIsAuthLoading(true); // Start loading console.log("[Auth Check] Starting..."); // Log start _a.label = 1; case 1: _a.trys.push([1, 7, 8, 9]); return [4 /*yield*/, fetch("".concat(process.env.NEXT_PUBLIC_BACKEND_URL, "/api/auth/student/status"), { credentials: "include" })]; case 2: response = _a.sent(); console.log("[Auth Check] Raw response status: ".concat(response.status)); // Log status if (!response.ok) return [3 /*break*/, 4]; return [4 /*yield*/, response.json()]; case 3: data = _a.sent(); console.log("[Auth Check] Response data:", data); // Log parsed data setIsAuthenticated(data.isAuthenticated); setStudent(data.student || null); // Store student data if (!data.isAuthenticated) { sonner_1.toast.error("Authentication required. Redirecting..."); console.log("[Auth Check] Not authenticated, redirecting to dashboard."); // Log redirect reason // Keep loading true while redirecting router.push("/student?teacherId=".concat(teacherId)); // No need to setLoading(false) here as the component will unmount return [2 /*return*/]; // Exit early } console.log("[Auth Check] Authenticated. Proceeding."); // Log success return [3 /*break*/, 6]; case 4: return [4 /*yield*/, response.text()]; case 5: errorText = _a.sent(); console.error("[Auth Check] Non-OK response: ".concat(response.status), errorText); // Log non-OK response setIsAuthenticated(false); setStudent(null); sonner_1.toast.error("Authentication check failed (".concat(response.status, "). Redirecting...")); console.log("[Auth Check] Non-OK response, redirecting to dashboard."); // Log redirect reason router.push("/student?teacherId=".concat(teacherId)); return [2 /*return*/]; // Exit early case 6: return [3 /*break*/, 9]; case 7: err_6 = _a.sent(); console.error("[Auth Check] Fetch error:", err_6); // Log fetch error setIsAuthenticated(false); setStudent(null); sonner_1.toast.error("Error checking authentication. Redirecting..."); console.log("[Auth Check] Fetch error, redirecting to dashboard."); // Log redirect reason router.push("/student?teacherId=".concat(teacherId)); return [2 /*return*/]; // Exit early case 8: // Only set loading false if we are NOT redirecting if (router.asPath.includes('/student/content/')) { // Check if we are still on the page console.log("[Auth Check] Finished, setting AuthLoading to false."); setIsAuthLoading(false); } else { console.log("[Auth Check] Finished, but already redirected."); } return [7 /*endfinally*/]; case 9: return [2 /*return*/]; } }); }); }; checkAuthStatus(); }, [apiBaseUrl, router, teacherId]); // Dependencies // Render the appropriate content based on the current item's type var renderContentItem = function () { var _a; if (!currentContentItem) { return ((0, jsx_runtime_1.jsx)("p", { className: "text-gray-500", children: "Select an item from the sidebar to view content." })); } var nextItem = findNextContentItem(); // Check if there is a next item var isLastItem = !nextItem; // Determine if this is the last item // --- LECTURE --- if (!("questions" in currentContentItem)) { // It's a Lecture var lecture_1 = currentContentItem; var contentType = lecture_1.type; // Parse content ONLY if it's expected to be JSON (video/h5p) var contentData = (contentType === 'video' || contentType === 'h5p') ? safeJsonParse(lecture_1.content) : null; // For text type, contentData is not needed, we use lecture.content directly return ((0, jsx_runtime_1.jsxs)("div", { children: [" ", (0, jsx_runtime_1.jsxs)(card_1.Card, { className: "mb-6 dark:bg-gray-800", children: [(0, jsx_runtime_1.jsx)(card_1.CardHeader, { children: (0, jsx_runtime_1.jsx)(card_1.CardTitle, { children: lecture_1.title }) }), (0, jsx_runtime_1.jsxs)(card_1.CardContent, { children: [contentType === "text" && ((0, jsx_runtime_1.jsx)("div", { className: "space-y-4 text-gray-700 dark:text-gray-300", children: (0, jsx_runtime_1.jsx)(react_markdown_1.default, { components: { code: function (_a) { var node = _a.node, inline = _a.inline, className = _a.className, children = _a.children, props = __rest(_a, ["node", "inline", "className", "children"]); var match = /language-(\w+)/.exec(className || ""); // Style differently for inline code vs code blocks return !inline ? ((0, jsx_runtime_1.jsx)("pre", { className: "bg-gray-100 dark:bg-gray-900 p-4 rounded-md overflow-x-auto my-4", children: (0, jsx_runtime_1.jsx)("code", __assign({ className: "font-mono text-sm ".concat(className) }, props, { children: typeof lecture_1.content === "string" ? lecture_1.content.replace(/\\n/g, "\n") : "Lecture content is not available." })) })) : ( // Inline code style (optional, can adjust) (0, jsx_runtime_1.jsx)("code", __assign({ className: "font-mono text-sm bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded ".concat(className) }, props, { children: children }))); }, }, children: typeof lecture_1.content === "string" ? lecture_1.content.replace(/\\n/g, "\n") : "Lecture content is not available." }) })), contentType === "video" && (contentData === null || contentData === void 0 ? void 0 : contentData.url) && ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("p", { className: "mb-2 text-sm text-gray-600", children: ["Video URL:", " ", (0, jsx_runtime_1.jsx)("a", { href: contentData.url, target: "_blank", rel: "noopener noreferrer", className: "text-indigo-600 hover:underline", children: contentData.url })] }), contentData.url.includes("youtube.com") || contentData.url.includes("youtu.be") ? ((0, jsx_runtime_1.jsx)("iframe", { width: "100%", height: "480" // Adjust height as needed